import processing.core.*; 
import processing.xml.*; 

import ddf.minim.*; 
import ddf.minim.signals.*; 
import ddf.minim.analysis.*; 
import ddf.minim.effects.*; 

import java.applet.*; 
import java.awt.Dimension; 
import java.awt.Frame; 
import java.awt.event.MouseEvent; 
import java.awt.event.KeyEvent; 
import java.awt.event.FocusEvent; 
import java.awt.Image; 
import java.io.*; 
import java.net.*; 
import java.text.*; 
import java.util.*; 
import java.util.zip.*; 
import java.util.regex.*; 

public class music_game_pde extends PApplet {





// *Had to use Processing ver. 1.2.1 (not 1.5) to export applet and application properly

//-----------------------------------------------------------
// MUSIC COMPOSITION EDITOR/GAME IN PROCESSING .pde
// Laura Park
// May 2011
// TIES Town & Country
//
/** This game is meant to introduce users to Western musical notation and composition.  
* No musical background necessary!
* Simply begin to compose your own music by pressing the arrow keys and Enter/Return key to play.
**/
// Details:
//  - First note is set to C5(middle C is C4)
//
//  - Current key commands: UP, DOWN, LEFT, RIGHT, ENTER
//    (Change pitch, change edit cursor, play all)
//
//  - Current sound choices: sine wave 
//
//  - Future expansion: eighth, half and whole notes; flats/sharps; 
//    play samples/instruments; multiple lines; time/measures; bass clef/polyphony
//---------------------------------------------------------------------

// For different sound choices, see AudioSample or AudioSnippet in Minim library (pretty good online documentation and examples)
// Audio files/samples will probably be necessary -  Wikipedia's entries on musical notes (like C, B, A..etc) have midi files of all those pitches
//   - make sure the audio is the correct pitch and also octave

// Polyphony could be implemented by having another key (like SPACE) designate a written note, then using LEFT go back and add a new Note object (and don't edit existing note at that index)
// --would probably need an array of notes for each index (so each note in the array is a note to be played simultaneously with the other notes in that array)

Minim minim;
AudioOutput out;

PImage bg; // staff bg  - add clefs

int maxN = 11; // number of notes allowed (in future, can expand to more lines)
int index = 0; // note index
Note[]notes = new Note[maxN]; // array of notes

int cPos = 114; // pitch C y-coord on line 1
//int n = 120;  // note's coordinates
int x,y;// cursor coordinates

PFont f,f2;
String message = "Press ENTER to play all the notes you've written!";
String usage1 = "Use right & left arrow keys to set position of current note."; 
String usage2 = "Use up & down keys to choose the note's pitch(frequency).";
String version = "version 1.0";

public void setup() {
  size(572, 204);//650, 600); // in future, can expand to more lines
  x=100;
  y=114; // initial coordinates for first note
  
  bg = loadImage("staffbg.png");  // load background image; file is in 'data' folder
  
  f = createFont("Arial",18,true);  // for messages
  f2 = createFont("Arial",14,true);
  
  minim = new Minim(this);  // audio library
  out = minim.getLineOut(Minim.STEREO);

  for (int j = 0; j < notes.length; j++) {
    notes[j] = new Note((100 + (j*45)), 114);
  } // create all Note objects (displayed in draw())
}

public void draw() { 
  background(255);
  image(bg,-10,40);  // place bg image
  
  // text messages
  fill(153); // version text in gray
  textFont(f2);         
  text(version, 488, 17);
  fill(0); //text messages in black
  textFont(f2);
  text(usage1, 8 ,17);
  text(usage2, 8 ,35);
  textFont(f);         
  text(message, 8, 54);
  
  // display notes
  notes[index].display();  // current note
  for(int d = 0; d < index; d++) {
   notes[d].display(); // past/written notes
  }
}   

// to update notes when user hits keys
public void keyReleased() {
 if (keyCode == DOWN) {
    // check for staff height
    if (y + 10 < 190) {
      float pitchChange = 1.05946f * 1.05946f; // whole step pitch 

      // check what original pitch, if C or F, move only half step
      if( (notes[index].getPitch() > 257 && notes[index].getPitch() < 266)  // C4 (middle C; 261.6 Hz)
       || (notes[index].getPitch() > 518 && notes[index].getPitch() < 528)  // C5
       || (notes[index].getPitch() > 345 && notes[index].getPitch() < 353)  // F4
       || (notes[index].getPitch() > 691 && notes[index].getPitch() < 704) ) { //F5
         pitchChange = 1.05946f;  // half step pitch (the 12th root of 2; there are 12 semitones)
       } 
  
      y = y + 10; // moves note up a half step
      keyCode = 0; // unnecessary?
      notes[index].moveY(y);  
      notes[index].setPitch((int) ( notes[index].getPitch() / pitchChange )); 
      
   // set new note letter
     if (notes[index].getLetter().equals("C")) {
       notes[index].setLetter("B");
     } else if (notes[index].getLetter().equals("B")) {
       notes[index].setLetter("A");
     } else if (notes[index].getLetter().equals("A")) {
       notes[index].setLetter("G");
     } else if (notes[index].getLetter().equals("G")) {
       notes[index].setLetter("F");
     } else if (notes[index].getLetter().equals("F")) {
       notes[index].setLetter("E");
     } else if (notes[index].getLetter().equals("E")) {
       notes[index].setLetter("D");
     } else if (notes[index].getLetter().equals("D")) {
       notes[index].setLetter("C");
     } 
      
      notes[index].playNote();
    }
 } 
 if(keyCode == UP) {
   // range check for staff height !!!!
   if (y - 10 > 55) {
     float pitchChange = 1.05946f * 1.05946f; // whole step pitch difference

      // check what original pitch, if B or E, move only half step
      if( (notes[index].getPitch() > 487 && notes[index].getPitch() < 498)  // B4
     // too low //  || (notes[index].getPitch() > 240 && notes[index].getPitch() < 251)  // B3 //
       || (notes[index].getPitch() > 325 && notes[index].getPitch() < 333)  // E4
       || (notes[index].getPitch() > 654 && notes[index].getPitch() < 664) ) { //E5
         pitchChange = 1.05946f;  // half step pitch (the 12th root of 2; there are 12 semitones)
       } 

     y = y - 10; // moves note down a half step
     keyCode = 0;// unnecessary?
     notes[index].moveY(y);  
     notes[index].setPitch((int) ( notes[index].getPitch()  * pitchChange)); 

  // set new note letter
     if (notes[index].getLetter().equals("C")) {
       notes[index].setLetter("D");
     } else if (notes[index].getLetter().equals("D")) {
       notes[index].setLetter("E");
     } else if (notes[index].getLetter().equals("E")) {
       notes[index].setLetter("F");
     } else if (notes[index].getLetter().equals("F")) {
       notes[index].setLetter("G");
     } else if (notes[index].getLetter().equals("G")) {
       notes[index].setLetter("A");
     } else if (notes[index].getLetter().equals("A")) {
       notes[index].setLetter("B");
     } else if (notes[index].getLetter().equals("B")) {
       notes[index].setLetter("C");
     } 
       
     notes[index].playNote();
   }
 }
 if(keyCode == RIGHT) { // moves edit cursor, will leave previous note written
   if ( x + 45 < width ) {
     // prep coordinates of next note
     x = x + 45;
     y = 114;
     keyCode = 0;
   //} //else { // new line
     //x = 100; 
     //y = y + n + 4; // 4 pixels padding btwn lines
   //}
   // can expand to more lines
     if ( index < maxN) index++; // move cursor to next note
   }
 }
 if (keyCode == LEFT) { // move edit cursor, now can edit pitch of previous note
   if (index > 0 ) {//if( x - 45 > 100) {
     x = x - 45;
     y = notes[index-1].getY(); // update cursor to previous note's pitch
     notes[index].reset(); // "erase"
    //else if ( y > 180 ) { // height of first staff
    // y = y - n - 4;  // go back up one line  -- how to implement editing?
   //} 
   // can expand to more lines
     index--;
   }
 }
 if(keyCode == ENTER || keyCode == RETURN){ // Play all written notes
    for(int i = 0; i <= index; i++) {
       notes[i].playNote();
       delay(300); 
    }
 }
}

//--------------------------------------------------------------
//  Note class
//--------------------------------------------------------------  
//  Each note has x and y coordinates
//                ellipse body and line stem
//                sine wave
//
//--------------------------------------------------------------
class Note {
  int xcoord, ycoord;
  int pitch;
  String letter;
  SineWave sine;
  
  Note(int xx, int yy) {
      xcoord = xx;
      ycoord = yy; 
      pitch = 262 * 2; // middle C is 262  
      letter = "C"; // note/pitch letter
      sine = new SineWave(pitch, 0.5f, out.sampleRate());
      sine.portamento(200);
  }

  // *Notes should not move horizontally, only the edit cursor 
  // moves left and right to select which note to edit
  //void moveX(int xx) {
  //   xcoord = xx;
  //}
  public void moveY(int yy) {
     ycoord = yy;
  }  
  public int getY() {
     return ycoord; 
  }
  public void display() {
     fill(0);  // put note body and stem in black
     ellipse(xcoord, ycoord, 30, 20);  // note body
     if (ycoord > 114) { // switch placement of note stem
       line (xcoord+15, ycoord, xcoord+15, ycoord-40); //stem
     } else {
       line (xcoord-15, ycoord, xcoord-15, ycoord+40); //stem
     }
     // conditional (on high B and middle C) slash through note
     if (ycoord == 64 || ycoord == 184) { 
       line (xcoord-25, ycoord, xcoord+25, ycoord);
     } 
     strokeWeight(4);
     
     fill(255); // put letter in white
     text(letter,xcoord-5,ycoord+5); // place note/pitch letter on top of note body
  }
  public int getPitch() {
     return pitch; 
  }    
  public void setPitch(int p) {
     pitch = p; 
     sine.setFreq(pitch); 
  }
  public void playNote() {
    out.addSignal(sine); 
    delay(500); // play for 0.5 s
    out.removeSignal(sine);
  }
  // Reset note to C5
  public void reset() {
    this.moveY(cPos); // "erase" by resetting pitch to C
    this.setPitch(262 * 2); 
    this.setLetter("C");
  }
  // Note Letter
  public String getLetter() {
    return letter;
  }
  public void setLetter(String l) {
    letter = l; 
  }
  
  //
  //void stopNote() {
  //   out.close();
  //   minim.stop(); 
  //}
}

//extra: eigth and half notes, sharps/flats
  static public void main(String args[]) {
    PApplet.main(new String[] { "--bgcolor=#F0F0F0", "music_game_pde" });
  }
}
